﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using CoreEx.Common.Extensions;
using CoreEx.Database.Persistence;
using CoreEx.DynamicInvoker;
using LinFu.IoC;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;

namespace CoreEx.Database.Persistence.Implementation
{
    [Implements(typeof (IStateTracker))]
    [Serializable]
    public class StateTracker : IStateTracker, IInitialize
    {
        private readonly IDictionary<object, IObjectState> _trackedObjects
            = new Dictionary<object, IObjectState>();

        private IStateTrackerRepository _stateTrackerRepository;
        public IPropertyValueExtractor PropertyValueExtractor { get; set; }

        public ITrackableListInjector TrackableListInjector { get; set; }

        #region IInitialize Members

        public void Initialize(IServiceContainer source)
        {
            PropertyValueExtractor = source.GetService<IPropertyValueExtractor>();
            TrackableListInjector = source.GetService<ITrackableListInjector>();
            _stateTrackerRepository = source.GetService<IStateTrackerRepository>();
        }

        #endregion

        #region IStateTracker Members

        public event EventHandler<ObjectRegisteredEventArgs> ObjectRegistered;


        public void Register(object target)
        {
            Register(target, StateType.NotModified);
        }

        public void Register(object target, StateType state)
        {
            if (IsTrackable(target) && !IsTrackedByAnyStateTracker(target))
            {
                StartTracking(target, state);

                EnsureEnumerablePropertiesAreTrackable(target);

                RegisterTrackableProperties(target);
            }
        }


        public IList<IObjectState> GetChanges()
        {
            return _trackedObjects.Where(kvp => kvp.Value.State != StateType.NotModified).Select(kvp => kvp.Value).ToList();
        }


        public IObjectState GetState(object target)
        {
            if (!Contains(target))
                throw new ArgumentOutOfRangeException("target", target, "Object is not registered in the state tracker");

            return _trackedObjects[target];
        }


        public bool Contains(object target)
        {
            return _trackedObjects.ContainsKey(target);
        }

        public void AcceptChanges()
        {
            ResetStateForAllTrackedObjects();
        }

        #endregion

        private void RegisterTrackableProperties(object target)
        {
            IEnumerable<PropertyInfo> properties = GetTrackableProperties(target);

            properties.ForEach(p => Register(p.DynamicInvoke(p, (object[]) target)));
        }

        private static IEnumerable<PropertyInfo> GetTrackableProperties(object target)
        {
            return target.GetType().GetProperties().Where(
                p => !p.PropertyType.IsEnumerable() && !p.PropertyType.IsSystemType());
        }

        private void StartTracking(object target, StateType initialState)
        {
            _trackedObjects.Add(target,
                                new ObjectState(target, initialState, PropertyValueExtractor.GetPropertyValues(target)));
            ((INotifyPropertyChanged) target).PropertyChanged += OnPropertyChanged;
            OnObjectRegistered(target);
        }

        private void EnsureEnumerablePropertiesAreTrackable(object target)
        {
            TrackableListInjector.Inject(target, this);
        }

        private bool IsTrackedByAnyStateTracker(object target)
        {
            return _stateTrackerRepository.IsTracked(target);
        }

        private static bool IsTrackable(object target)
        {
            return target != null && typeof (INotifyPropertyChanged).IsAssignableFrom(target.GetType());
        }


        private void OnObjectRegistered<T>(T target) where T : class
        {
            if (ObjectRegistered != null)
                ObjectRegistered(this, new ObjectRegisteredEventArgs(target));
        }


        private void ResetStateForAllTrackedObjects()
        {
            _trackedObjects.ForEach(t => t.Value.State = StateType.NotModified);

            _trackedObjects.ForEach(
                t => t.Value.OriginalValues = PropertyValueExtractor.GetPropertyValues(t.Value.Target));
        }


        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            ApplyNewState(sender);

            //Make sure that assigned trackable objects are registered
            Register(GetValueByPropertyName(sender, e.PropertyName));
        }

        private IObjectState GetCurrentObjectState(object target)
        {
            return _trackedObjects[target];
        }

        private static object GetValueByPropertyName(object sender, string propertyName)
        {
            return sender.GetType().DynamicInvoke(sender, propertyName);
        }

        private void ApplyNewState(object target)
        {
            IObjectState objectState = GetCurrentObjectState(target);

            if (objectState.State == StateType.New)
                objectState.State = StateType.NewModified;
            if (objectState.State == StateType.NotModified)
                objectState.State = StateType.Modified;
        }
    }
}